package solver

import (
	"fmt"
	"gosudoku/set"
	"log"
	"slices"
	"strings"
)

func (g *Grid) BinaryUniversalGrave1() (used bool) {
	triples := g.scanBiValueCells()
	if len(triples) != 1 {
		return false
	}
	truceCandCells, trueCands := g.searchTrueCands(triples)
	if len(truceCandCells) != 1 || len(trueCands) != 1 || trueCands[0].Count() != 1 {
		return false
	}

	cell := truceCandCells[0]
	value := toValue(trueCands[0].GetList()[0])
	g.solveOneCell(cell.rowNum, cell.colNum, value)

	log.Printf("r%vc%v %v Binary Universal Grave Type 1",
		cell.rowNum, cell.colNum, value)

	return true
}

func (g *Grid) BinaryUniversalGrave2() (used bool) {
	triples := g.scanBiValueCells()
	if len(triples) < 2 {
		return
	}
	ok, cand := g.verifyBug2(triples)
	if !ok {
		return false
	}

	if !g.clearupSeenCand(cand, triples) {
		return
	}

	logBUG2(triples, cand, "Binary Universal Grave Type 2")
	return true
}

func logBUG2(cells []*Cell, cand int, sname string) {
	var b strings.Builder
	logCells(&b, cells)
	fmt.Fprintf(&b, "%v %v", toValue(cand), sname)
	log.Print(b.String())
}

func (g *Grid) verifyBug2(triples []*Cell) (ok bool, trueCand int) {
	trueCandCells, trueCands := g.searchTrueCands(triples)
	if len(trueCandCells) != len(triples) {
		return
	}
	tc := trueCands[0]
	if tc.Count() != 1 {
		return
	}
	for _, cand := range trueCands[1:] {
		if cand != tc {
			return
		}
	}
	ok = true
	trueCand = tc.GetList()[0]
	return
}

// there is a true candidate which is different from the others
func (g *Grid) BinaryUniversalGraveN() (used bool) {
	triples := g.scanBiValueCells()
	//fmt.Println(triples)

	trueCandCells, trueCands := g.searchTrueCands(triples)
	if len(trueCandCells) != len(triples) {
		return
	}

	//fmt.Println(trueCandCells, trueCands)
	if len(trueCandCells) == 1 && trueCands[0].Count() > 1 {
		cell := trueCandCells[0]
		cands := trueCands[0].GetList()
		cell.removeCandsBut(cands)

		logBUGN(cell, trueCands[0], []*Cell{}, set.Set{})
		return true
	}

	if len(triples) <= 1 {
		return false
	}

	trueCandCellsMap := map[set.Set][]*Cell{}
	for i := 0; i < len(trueCands); i++ {
		trueCandCellsMap[trueCands[i]] = append(trueCandCellsMap[trueCands[i]], trueCandCells[i])
	}

	if len(trueCandCellsMap) != 2 {
		return
	}

	//fmt.Println(trueCandCellsMap)

	var singleCells []*Cell
	var singleCands set.Set
	var multiCells []*Cell
	var multiCands set.Set

	keys := []set.Set{}
	values := [][]*Cell{}
	for cands, cells := range trueCandCellsMap {
		keys = append(keys, cands)
		values = append(values, cells)
	}

	if len(values[0]) == 1 {
		singleCells = values[0]
		singleCands = keys[0]
		multiCells = values[1]
		multiCands = keys[1]
	} else {
		singleCells = values[1]
		singleCands = keys[1]
		multiCells = values[0]
		multiCands = keys[0]
	}

	//fmt.Println(singleCells, singleCands, multiCells, multiCands)

	if len(singleCells) != 1 || len(multiCells) < 1 {
		return
	}

	if multiCands.Count() != 1 {
		return
	}

	if !isSeenBy(singleCells[0], multiCells) {
		return
	}

	if singleCands.Intersect(multiCands).Count() == 0 {
		if singleCells[0].removeCand(multiCands.GetList()[0]) {
			used = true
		}
	}

	if len(triples) == 2 && singleCands.Count() == 1 {
		if multiCells[0].removeCand(singleCands.GetList()[0]) {
			used = true
		}
	}
	if used {
		logBUGN(singleCells[0], singleCands,
			multiCells, multiCands)
	}
	return
}

func (g *Grid) searchTrueCands(triples []*Cell) (
	trueCandCells []*Cell, trueCands []set.Set) {
	change := true
	for change {
		change = false
		for _, triple := range triples {
			if slices.Contains(trueCandCells, triple) {
				continue
			}
			cands := g.fetchTrueCands(triple, trueCandCells, trueCands)
			if cands.Count() == triple.cands.Count()-2 {
				trueCandCells = append(trueCandCells, triple)
				trueCands = append(trueCands, cands)
				change = true
			}
		}
	}
	if len(trueCandCells) != len(triples) {
		return []*Cell{}, []set.Set{}
	}
	return trueCandCells, trueCands
}

func (g *Grid) scanBiValueCells() []*Cell {
	cells := []*Cell{}
	for _, row := range g.rows {
		for _, cell := range row.cells {
			if cell.solved() {
				continue
			}
			count := cell.cands.Count()
			if count == 2 {
				continue
			}
			if count >= 3 {
				cells = append(cells, cell)
				continue
			}
			return []*Cell{}
		}
	}
	return cells
}

func isSeenBy(seenCell *Cell, cells []*Cell) bool {
	for _, cell := range cells {
		if seenCell.rowNum != cell.rowNum &&
			seenCell.colNum != cell.colNum &&
			seenCell.boxNum != cell.boxNum {
			return false
		}
	}
	return true
}

func (g *Grid) fetchTrueCands(cell *Cell, trueCandCells []*Cell, trueCands []set.Set) (
	cands set.Set) {
	candsr := g.fetchTrueCandsInHouse(cell, trueCandCells, trueCands, g.rows[cell.rowNum])
	candsc := g.fetchTrueCandsInHouse(cell, trueCandCells, trueCands, g.cols[cell.colNum])
	candsb := g.fetchTrueCandsInHouse(cell, trueCandCells, trueCands, g.boxes[cell.boxNum])
	cands = candsr.Intersect(candsc).Intersect(candsb)
	return
}

func (g *Grid) fetchTrueCandsInHouse(theCell *Cell, trueCandCells []*Cell,
	trueCands []set.Set, house House) (cands set.Set) {
	candCounts := [9]int{}
	for _, cell := range house.cells {
		if cell.solved() {
			continue
		}
		for _, cand := range cell.cands.GetList() {
			skip := false
			for i, t := range trueCandCells {
				if cell == t && trueCands[i].Has(cand) {
					skip = true
					break
				}
			}
			if skip {
				continue
			}
			candCounts[cand]++
		}
	}
	for cand, count := range candCounts {
		if count == 2 || count == 0 {
			continue
		}
		if count > 2 && theCell.cands.Has(cand) {
			cands.Add(cand)
		}
	}
	return
}

func logBUGN(cell *Cell, cand set.Set, cells []*Cell, cands set.Set) {
	var b strings.Builder
	N := cand.Count() + len(cells)
	fmt.Fprintf(&b, "r%vc%v %v ", cell.rowNum, cell.colNum, cand.ValueString())
	logCells(&b, cells)
	fmt.Fprintf(&b, "%v Binary Universal Grave+%v", cands.ValueString(), N)
	log.Print(b.String())
}

func (g *Grid) BinaryUniversalGrave3Naked() (used bool) {
	return g.binaryUniversalGrave3(BUG3Naked)
}

func (g *Grid) BinaryUniversalGrave3Hidden() (used bool) {
	return g.binaryUniversalGrave3(BUG3Hidden)
}

func (g *Grid) binaryUniversalGrave3(bug3Func func([]*Cell, []set.Set, House) bool) (used bool) {
	triples := g.scanBiValueCells()
	if len(triples) < 2 {
		return
	}

	trueCandCells, trueCands := g.searchTrueCands(triples)
	if len(trueCandCells) != len(triples) {
		return
	}

	row := g.rows[trueCandCells[0].rowNum]
	if inHouse(trueCandCells, row) {
		if bug3Func(trueCandCells, trueCands, row) {
			return true
		}
	}
	col := g.cols[trueCandCells[0].colNum]
	if inHouse(trueCandCells, col) {
		if bug3Func(trueCandCells, trueCands, col) {
			return true
		}
	}
	box := g.boxes[trueCandCells[0].boxNum]
	if inHouse(trueCandCells, box) {
		if bug3Func(trueCandCells, trueCands, box) {
			return true
		}
	}

	return
}

func inHouse(cells []*Cell, house House) bool {
	switch house.kind {
	case 0:
		for _, cell := range cells {
			if cell.rowNum != house.cells[0].rowNum {
				return false
			}
		}
		return true
	case 1:
		for _, cell := range cells {
			if cell.colNum != house.cells[0].colNum {
				return false
			}
		}
		return true
	case 2:
		for _, cell := range cells {
			if cell.boxNum != house.cells[0].boxNum {
				return false
			}
		}
		return true

	}
	return false
}

func BUG3Naked(trueCandCells []*Cell, trueCands []set.Set, house House) (used bool) {
	unSolovedCells := []*Cell{}
	for _, cell := range house.cells {
		if cell.solved() || slices.Contains(trueCandCells, cell) {
			continue
		}
		unSolovedCells = append(unSolovedCells, cell)
	}

	cands := set.NewSet()
	for _, trueCand := range trueCands {
		cands = cands.Union(trueCand)
	}
	start := cands.Count() - 1
	end := len(unSolovedCells) / 2
	//fmt.Println(start, end, cands, unSolovedCells)

	for count := start; count <= end; count++ {
		for _, cells := range combine(unSolovedCells, count) {
			//fmt.Println(cells)
			ok, nakedCands := verifyBug3Naked(cands, cells)
			if !ok {
				continue
			}
			for _, cell := range unSolovedCells {
				if slices.Contains(cells, cell) {
					continue
				}
				if cell.removeCands(cands.GetList()) {
					used = true
				}
			}
			if used {
				logBUG3(trueCandCells, cands, cells, nakedCands,
					"Binary Universal Grave Type 3 Naked")
				return
			}
		}
	}
	return
}

func BUG3Hidden(trueCandCells []*Cell, trueCands []set.Set, house House) (used bool) {
	unSolovedCells := []*Cell{}
	for _, cell := range house.cells {
		if cell.solved() || slices.Contains(trueCandCells, cell) {
			continue
		}
		unSolovedCells = append(unSolovedCells, cell)
	}

	cands := set.NewSet()
	for _, cell := range trueCandCells {
		cands = cands.Union(cell.cands)
	}
	trues := set.NewSet()
	for _, trueCand := range trueCands {
		trues = trues.Union(trueCand)
	}
	cands = cands.Diff(trues)
	start := 1
	end := len(unSolovedCells) / 2

	for count := start; count <= end; count++ {
		for _, cells := range combine(unSolovedCells, count) {
			ok, hiddenCands := verifyBug3Hidden(cands, cells, unSolovedCells)

			if !ok {
				continue
			}
			for _, cell := range cells {
				if cell.removeCandsBut(hiddenCands.GetList()) {
					used = true
				}
			}
			if used {
				logBUG3(trueCandCells, trues, cells, hiddenCands,
					"Binary Universal Grave Type 3 Hidden")
				return
			}
		}
	}
	return
}

func verifyBug3Hidden(cands set.Set, cells, unSolovedCells []*Cell) (
	ok bool, hiddenCands set.Set) {
	hiddenCands = cands
	otherCands := set.NewSet()
	for _, cell := range unSolovedCells {
		if slices.Contains(cells, cell) {
			hiddenCands = hiddenCands.Union(cell.cands)
		} else {
			otherCands = otherCands.Union(cell.cands)
		}
	}
	hiddenCands = hiddenCands.Diff(otherCands)
	ok = hiddenCands.Count() == len(cells)+1
	return
}

func verifyBug3Naked(cands set.Set, cells []*Cell) (ok bool, nakedCands set.Set) {
	nakedCands = cands
	for _, cell := range cells {
		nakedCands = nakedCands.Union(cell.cands)
	}
	ok = nakedCands.Count() == len(cells)+1
	return
}

func logBUG3(trueCandCells []*Cell, trueCands set.Set,
	nakedCells []*Cell, nakedCands set.Set, sname string) {
	var b strings.Builder

	logCells(&b, trueCandCells)
	fmt.Fprintf(&b, " %v ", trueCands.ValueString())
	logCells(&b, nakedCells)
	fmt.Fprintf(&b, " %v %v", nakedCands.ValueString(), sname)
	log.Print(b.String())
}

func (g *Grid) BinaryUniversalGrave4() (used bool) {
	triples := g.scanBiValueCells()
	if len(triples) != 2 {
		return
	}

	trueCandCells, trueCands := g.searchTrueCands(triples)
	if len(trueCandCells) != len(triples) {
		return
	}

	var house House
	if triples[0].rowNum == triples[1].rowNum {
		house = g.rows[triples[0].rowNum]
	} else if triples[0].colNum == triples[1].colNum {
		house = g.cols[triples[0].colNum]
	} else if triples[0].boxNum == triples[1].boxNum {
		house = g.boxes[triples[0].boxNum]
	} else {
		return
	}

	cands := set.NewSet()
	otherCands := set.NewSet()
	for _, cell := range house.cells {
		if cell.solved() {
			continue
		}
		if slices.Contains(trueCandCells, cell) {
			cands = cands.Union(cell.cands)
		} else {
			otherCands = otherCands.Union(cell.cands)
		}
	}
	trues := set.NewSet()
	for _, cand := range trueCands {
		trues = trues.Union(cand)
	}
	cands = cands.Diff(trues).Diff(otherCands)
	if cands.Count() != 1 {
		return
	}

	for i, cell := range trueCandCells {
		keepCands := cands.Union(trueCands[i])
		if cell.removeCandsBut(keepCands.GetList()) {
			used = true
		}
	}

	if used {
		logBUG4(trueCandCells, trues, cands)
	}

	return
}

func (g *Grid) BinaryUniversalGraveXZ() (used bool) {
	triples := g.scanBiValueCells()
	if len(triples) != 2 {
		return
	}

	if sameHouse(triples) {
		return
	}

	trueCandCells, trueCands := g.searchTrueCands(triples)
	if len(trueCandCells) != len(triples) {
		return
	}

	if trueCands[0].Count() != 1 || trueCands[1].Count() != 1 {
		return
	}

	cands := trueCands[0].Union(trueCands[1])
	if cands.Count() != 2 {
		return
	}

	for i, cell := range trueCandCells {
		for _, seen := range g.seenCells(cell) {
			if seen.cands == cands {
				cand := trueCands[1-i].GetList()[0]
				cells := []*Cell{trueCandCells[1-i], seen}
				if g.clearupSeenCand(cand, cells) {
					logBUGXZ(trueCandCells, cands, seen)
					used = true
					return
				}
			}
		}
	}
	return
}

func logBUG4(trueCandCells []*Cell, trues set.Set, cands set.Set) {
	var b strings.Builder

	logCells(&b, trueCandCells)
	fmt.Fprintf(&b, " %v ", trues.ValueString())
	fmt.Fprintf(&b, " %v Binary Universal Grave Type 4", cands.ValueString())
	log.Print(b.String())
}

func logBUGXZ(trueCandCells []*Cell, cands set.Set, seen *Cell) {
	var b strings.Builder

	logCells(&b, trueCandCells)
	fmt.Fprintf(&b, "%v ", cands.ValueString())
	logCells(&b, []*Cell{seen})
	fmt.Fprintf(&b, "Binary Universal Grave XZ")
	log.Print(b.String())
}
